3.1 View 基础知识
3.1.1 什么是 View
View 是 Android 中所有控件的基类,View 是一种界面层的控件的一种抽象,它代表了一个控件(如:Button、TextView)。除了 View 还有 ViewGroup,可以理解为控件组,它内部包含了许多个控件,即一组 View。在 Android 设计中,ViewGroup 也继承了 View,这就意味 View 本身就可以是单个控件也可以是多个控件组成的一组控件,通过这种关系就形成了 View 树,这和 Web 前端中的 DOM 树的概念是相识的。
3.1.2 View 的位置参数
View 的位置主要由它的四个定点来决定,分别对应于 View 的四个属性:top、left、right、bottom。在 Android 中,x 轴和 y 轴的正方向分别为右和下。
根据 View 的宽高和坐标的关系,得出:
width = right - left |
解释部分参数:
// View 的四个属性 |
3.1.3 MotionEvent 和 TouchSlop
MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
- ACTION_DOWM 手指刚接触屏幕
- ACTION_MOVE 手指在屏幕上移动
- ACTION_UP 手指从屏幕上松开的一瞬间
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种形况:
- 点击屏幕后离开松开,事件序列为 DOWN -> UP
- 点击屏幕滑动一会儿再松开,事件序列为 DOWN -> MOVE -> …. > MOVE -> UP
TouchSlop
TouchSlop 是系统所能识别出的被认为是滑动的最小距离。当手指在屏幕上滑动,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。
通过以下方式获取这个常量:
ViewConfiguration.get(getContext()).getScaledTouchSlop();
3.1.4 VelocityTracker、GestureDetector 和 Scroller
VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。速度的计算公式如下:
速度 = (终点位置 - 起点位置)/ 时间段
使用方式:
// 首先在 View 的 onTouchEvent 方法中追踪当前单击事件的速度
VelocityTracker vt = VelocityTracker.obtain();
vt.addMovement(event);
// 计算速度
vt.computeCurrentVelocity(1000);
// 获取速度
getXVelocity();
getYVelocity()
// 重置和回收
vt.clear();
vt.recycler();GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。如果只是监听滑动相关的,建议自己在 onTouchEvent 中实现,如果想要监听双击这种行为,那么就用 GestureDetecort。
使用方式:
GestureDetector mGes = new GestureDetector(this);
// 接管目标 View 的 onTouchEvent 方法
boolean consume = mGes.onTouchEvent(event);
return consume;Scroller
弹性滑动对象,用于实现View的弹性滑动。Scroller本身无法让View弹性滑动,它需要和 View 的computeScroll 方法配合使用才能共同完成这个功能。
使用方式:
Scroller mScroller = new Scroller(getContext());
// 缓慢滑动到指定位置,1000 ms
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
3.2 View 的滑动
通过三种方式可以实现 View 的滑动
- 第一种是通过 View 本身提供的 scrollTo/scrollBy 方法来实现滑动
- 第二种是通过动画给 View 施加平移效果来实现滑动
- 通过改变 View 的 LayoutParams 使得 View 重新布局从而实现滑动
3.2.1 使用 scrollTo/scrollBy
scrollTo 和 scrollBy 方法只能改变 view 内容的位置而不能改变 view 在布局中的位置。 scrollBy 是基于当前位置的相对滑动,而 scrollTo 是基于所传参数的绝对滑动。通过 View 的 getScrollX 和 getScrollY 方法可以得到滑动的距离。
3.2.2 使用动画
使用动画来移动 View 主要是操作view的 translationX 和 translationY 属性,既可以使用传统的 View 动画,也可以使用属性动画。
3.2.3 改变布局参数
通过改变 LayoutParams 的方式去实现 View 的滑动是一种灵活的方法。
3.2.4 各种滑动方式的对比
- scrollTo/scrollBy:操作简单,适合对 View 内容的滑动
- 动画:操作简单,主要适用于没有交互的 View 和实现复杂的动画效果
- 改变布局参数:操作稍微复杂,适用于有交互的 View
3.4 View 的事件分发机制
3.4.1 事件分发机制的三个重要方法
- public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前的 View,那么此方法一定会被调用,返回结果受当前 View 的onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
- public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法内部调用,用来判断是否拦截某个事件,如果当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
- public boolean onTouchEvent(MotionEvent event)
在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前的事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接受到事件。
这三个方法的关系可以用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent event) |
我们可以大致了解点击事件的传递规则:对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法就会被调用;如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。
OnTouchListener 的优先级比 onTouchEvent 要高
如果给一个 View 设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回 false,那么当前 View 的 onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。 在 onTouchEvent 方法中,如果当前 View 设置了 OnClickListener,那么它的 onClick 方法会被调用,所以 OnClickListener 的优先级最低。
当点击一个事件产生后,它的传递过程遵循如顺序,Activity -> Window -> View
如果一个 View 的 onTouchEvent 方法返回 false,那么它的父容器的 onTouchEvent 方法将会被调用,依次类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理(调用 Activity 的onTouchEvent 方法)
关于事件传递的机制,给出一些结论:
- 同一个事件序列是以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束
- 正常情况下,一个事件序列只能被一个 View 拦截且消耗。一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件通过 onTouchEvent 强行传递给其他 View 处理
- 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,那么同一事件序列的其他事情都不会再交给它来处理,并且事件将重新交给它的父容器去处理(调用父容器的 onTouchEvent 方法);如果它消耗 ACTION_DOWN 事件,但是不消耗其他类型事件,那么这个点击事件会消失,父容器的 onTouchEvent 方法不会被调用,当前 View 依然可以收到后续的事件,但是这些事件最后都会传递给 Activity 处理。
- ViewGroup 默认不拦截任何事件。Android 源码中 ViewGroup 的 onInterceptTouchEvent 方法默认返回false,View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会调用。
- View 的 onTouchEvent 默认都会消耗事件(返回true),除非它是不可点击的(clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认都为 false,clickable 要分情况,比如 Button 的 clickable 属性默认为 true,而 TextView 的 clickable 属性默认为 false。
- View 的 enable 属性不影响 onTouchEvent 的默认返回值,哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true
- 事件传递过程总是先传递给父元素,然后再由父元素分发给子 View,通过requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN 事件除外,即当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的onInterceptTouchEvent 方法来询问自己是否要拦截事件。
3.5 View 的滑动冲突
3.5.1 常见的滑动冲突场景
- 外部滑动方向与内部滑动方向不一致,比如 ViewPager 中包含 ListView
- 外部滑动方向与内部滑动方向一致
- 上面两种情况的嵌套
3.5.2 滑动冲突的处理规则
可以根据滑动距离和水平方向形成的夹角;或者根据水平和竖直方向滑动的距离差;或者两个方向上的速度差等。
3.5.3 滑动冲突的解决方式
- 外部拦截法
点击事件都经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,该方法需要重写父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可,伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event) { |
- 内部拦截法
父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就由父容器进行处理,这种方法和 Android 中的事件分发机制不一样,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作。
public boolean dispatchTouchEvent(MotionEvent event) { |